{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Execute this cell to install dependencies\n",
    "%pip install sf-hamilton[visualization]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# MLFlow plugin tutorial [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/dagworks-inc/hamilton/blob/main/examples/mlflow/tutorial.ipynb) [![GitHub badge](https://img.shields.io/badge/github-view_source-2b3137?logo=github)](https://github.com/apache/hamilton/blob/main/examples/mlflow/tutorial.ipynb)\n",
    "\n",
    "This notebook shows to use the MLFlow plugin for Hamilton. The first three sections present minimal examples to introduce the core functionalities:\n",
    "1. Training and saving a model with `MLFlowModelSaver`\n",
    "2. Loading a model for inference with `MLFlowModelLoader`\n",
    "3. Automatically tracking execution results with `MLFlowTracker`\n",
    "\n",
    "The following sections give details about individual features.   "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Load the [notebook extension](https://github.com/apache/hamilton/tree/main/examples/jupyter_notebook_magic) for Hamilton. It allows us to define a dataflow in a code cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext hamilton.plugins.jupyter_magic"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Training and saving a model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.1 Define\n",
    "We define a simple dataflow that loads the titanic dataset and trains a logistic regression to predict survival. The function parameters specify the dependencies between nodes of the dataflow.\n",
    "\n",
    "The first line of the cell `%%cell_to_module model_training --display` is related to the notebook extension and means this cell will define a self-contained Python module named `model_training`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"423pt\" height=\"208pt\"\n",
       " viewBox=\"0.00 0.00 422.50 208.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 204)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-204 418.5,-204 418.5,4 -4,4\"/>\n",
       "<g id=\"clust1\" class=\"cluster\">\n",
       "<title>cluster__legend</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" points=\"8,-115 8,-192 104,-192 104,-115 8,-115\"/>\n",
       "<text text-anchor=\"middle\" x=\"56\" y=\"-176.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">Legend</text>\n",
       "</g>\n",
       "<!-- X -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>X</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M221.5,-146C221.5,-146 146.5,-146 146.5,-146 140.5,-146 134.5,-140 134.5,-134 134.5,-134 134.5,-94 134.5,-94 134.5,-88 140.5,-82 146.5,-82 146.5,-82 221.5,-82 221.5,-82 227.5,-82 233.5,-88 233.5,-94 233.5,-94 233.5,-134 233.5,-134 233.5,-140 227.5,-146 221.5,-146\"/>\n",
       "<text text-anchor=\"start\" x=\"178.5\" y=\"-124.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">X</text>\n",
       "<text text-anchor=\"start\" x=\"145.5\" y=\"-96.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- trained_model -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>trained_model</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M402.5,-105C402.5,-105 274.5,-105 274.5,-105 268.5,-105 262.5,-99 262.5,-93 262.5,-93 262.5,-53 262.5,-53 262.5,-47 268.5,-41 274.5,-41 274.5,-41 402.5,-41 402.5,-41 408.5,-41 414.5,-47 414.5,-53 414.5,-53 414.5,-93 414.5,-93 414.5,-99 408.5,-105 402.5,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"282.5\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">trained_model</text>\n",
       "<text text-anchor=\"start\" x=\"273.5\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">LogisticRegression</text>\n",
       "</g>\n",
       "<!-- X&#45;&gt;trained_model -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>X&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M233.67,-100.91C239.68,-99.3 245.95,-97.61 252.3,-95.91\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"253.58,-99.19 262.33,-93.21 251.77,-92.43 253.58,-99.19\"/>\n",
       "</g>\n",
       "<!-- y -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>y</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M204.5,-64C204.5,-64 163.5,-64 163.5,-64 157.5,-64 151.5,-58 151.5,-52 151.5,-52 151.5,-12 151.5,-12 151.5,-6 157.5,0 163.5,0 163.5,0 204.5,0 204.5,0 210.5,0 216.5,-6 216.5,-12 216.5,-12 216.5,-52 216.5,-52 216.5,-58 210.5,-64 204.5,-64\"/>\n",
       "<text text-anchor=\"start\" x=\"179\" y=\"-42.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">y</text>\n",
       "<text text-anchor=\"start\" x=\"162.5\" y=\"-14.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- y&#45;&gt;trained_model -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>y&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M216.6,-40.5C227.23,-43.35 239.66,-46.7 252.42,-50.13\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"251.66,-53.54 262.22,-52.76 253.47,-46.78 251.66,-53.54\"/>\n",
       "</g>\n",
       "<!-- load_data -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>load_data</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M93.5,-105C93.5,-105 18.5,-105 18.5,-105 12.5,-105 6.5,-99 6.5,-93 6.5,-93 6.5,-53 6.5,-53 6.5,-47 12.5,-41 18.5,-41 18.5,-41 93.5,-41 93.5,-41 99.5,-41 105.5,-47 105.5,-53 105.5,-53 105.5,-93 105.5,-93 105.5,-99 99.5,-105 93.5,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"17.5\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">load_data</text>\n",
       "<text text-anchor=\"start\" x=\"43\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;X -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;X</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M105.71,-88.85C111.9,-90.86 118.27,-92.94 124.58,-94.99\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"123.82,-98.42 134.41,-98.19 125.99,-91.77 123.82,-98.42\"/>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;y -->\n",
       "<g id=\"edge4\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;y</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M105.71,-57.15C117.5,-53.31 129.99,-49.25 141.41,-45.53\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"142.7,-48.79 151.13,-42.37 140.53,-42.14 142.7,-48.79\"/>\n",
       "</g>\n",
       "<!-- function -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>function</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M84,-160.5C84,-160.5 28,-160.5 28,-160.5 22,-160.5 16,-154.5 16,-148.5 16,-148.5 16,-135.5 16,-135.5 16,-129.5 22,-123.5 28,-123.5 28,-123.5 84,-123.5 84,-123.5 90,-123.5 96,-129.5 96,-135.5 96,-135.5 96,-148.5 96,-148.5 96,-154.5 90,-160.5 84,-160.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"56\" y=\"-138.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">function</text>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<graphviz.graphs.Digraph at 0x7f89dc6b33d0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%%cell_to_module model_training --display\n",
    "import pandas as pd\n",
    "from sklearn.base import BaseEstimator\n",
    "from sklearn.datasets import fetch_openml\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from hamilton.function_modifiers import extract_fields\n",
    "\n",
    "# split the returned dictionary into 2 nodes: `X` and `y`\n",
    "@extract_fields(dict(X=pd.DataFrame, y=pd.Series))\n",
    "def load_data() -> dict:\n",
    "    \"\"\"Load the titanic dataset and split it in X and y. \n",
    "    Only keep the columns `fare` and `age` and fill null values.\n",
    "    \"\"\"\n",
    "    X, y = fetch_openml(\"titanic\", version=1, as_frame=True, return_X_y=True)\n",
    "    X = X[[\"fare\", \"age\"]].fillna(0)\n",
    "    return dict(X=X, y=y)\n",
    "\n",
    "def trained_model(X: pd.DataFrame, y: pd.Series) -> LogisticRegression:\n",
    "    \"\"\"Fit a binary classifier on the data\"\"\"\n",
    "    model = LogisticRegression()\n",
    "    model.fit(X, y)\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.2 Assemble\n",
    "To execute code, we build the `Driver` with the module `model_training` defined in the previous cell. \n",
    "\n",
    "The statement `to.mlflow()` creates a `MLFlowModelSaver` that registers the model returned by `trained_model()` as `my_predictor` in the MLFlow model registry. We add this to the `Driver` using\n",
    "`.with_materializers()`. A new node will be displayed in the visualization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"667pt\" height=\"265pt\"\n",
       " viewBox=\"0.00 0.00 667.00 265.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 261)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-261 663,-261 663,4 -4,4\"/>\n",
       "<g id=\"clust1\" class=\"cluster\">\n",
       "<title>cluster__legend</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" points=\"8,-115 8,-249 129,-249 129,-115 8,-115\"/>\n",
       "<text text-anchor=\"middle\" x=\"68.5\" y=\"-233.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">Legend</text>\n",
       "</g>\n",
       "<!-- y -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>y</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M220,-146C220,-146 179,-146 179,-146 173,-146 167,-140 167,-134 167,-134 167,-94 167,-94 167,-88 173,-82 179,-82 179,-82 220,-82 220,-82 226,-82 232,-88 232,-94 232,-94 232,-134 232,-134 232,-140 226,-146 220,-146\"/>\n",
       "<text text-anchor=\"start\" x=\"194.5\" y=\"-124.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">y</text>\n",
       "<text text-anchor=\"start\" x=\"178\" y=\"-96.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- trained_model -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>trained_model</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M418,-105C418,-105 290,-105 290,-105 284,-105 278,-99 278,-93 278,-93 278,-53 278,-53 278,-47 284,-41 290,-41 290,-41 418,-41 418,-41 424,-41 430,-47 430,-53 430,-53 430,-93 430,-93 430,-99 424,-105 418,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"298\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">trained_model</text>\n",
       "<text text-anchor=\"start\" x=\"289\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">LogisticRegression</text>\n",
       "</g>\n",
       "<!-- y&#45;&gt;trained_model -->\n",
       "<g id=\"edge5\" class=\"edge\">\n",
       "<title>y&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M232.1,-105.5C242.73,-102.65 255.16,-99.3 267.92,-95.87\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"268.97,-99.22 277.72,-93.24 267.16,-92.46 268.97,-99.22\"/>\n",
       "</g>\n",
       "<!-- X -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>X</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M237,-64C237,-64 162,-64 162,-64 156,-64 150,-58 150,-52 150,-52 150,-12 150,-12 150,-6 156,0 162,0 162,0 237,0 237,0 243,0 249,-6 249,-12 249,-12 249,-52 249,-52 249,-58 243,-64 237,-64\"/>\n",
       "<text text-anchor=\"start\" x=\"194\" y=\"-42.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">X</text>\n",
       "<text text-anchor=\"start\" x=\"161\" y=\"-14.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- X&#45;&gt;trained_model -->\n",
       "<g id=\"edge4\" class=\"edge\">\n",
       "<title>X&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M249.17,-45.09C255.18,-46.7 261.45,-48.39 267.8,-50.09\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"267.27,-53.57 277.83,-52.79 269.08,-46.81 267.27,-53.57\"/>\n",
       "</g>\n",
       "<!-- trained_model__mlflow -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>trained_model__mlflow</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M659,-109C659,-113.41 614.18,-117 559,-117 503.82,-117 459,-113.41 459,-109 459,-109 459,-37 459,-37 459,-32.59 503.82,-29 559,-29 614.18,-29 659,-32.59 659,-37 659,-37 659,-109 659,-109\"/>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M659,-109C659,-104.59 614.18,-101 559,-101 503.82,-101 459,-104.59 459,-109\"/>\n",
       "<text text-anchor=\"start\" x=\"470\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">trained_model__mlflow</text>\n",
       "<text text-anchor=\"start\" x=\"492.5\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">MLFlowModelSaver</text>\n",
       "</g>\n",
       "<!-- trained_model&#45;&gt;trained_model__mlflow -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>trained_model&#45;&gt;trained_model__mlflow</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M430.01,-73C436.02,-73 442.16,-73 448.34,-73\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"448.76,-76.5 458.76,-73 448.76,-69.5 448.76,-76.5\"/>\n",
       "</g>\n",
       "<!-- load_data -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>load_data</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M106,-105C106,-105 31,-105 31,-105 25,-105 19,-99 19,-93 19,-93 19,-53 19,-53 19,-47 25,-41 31,-41 31,-41 106,-41 106,-41 112,-41 118,-47 118,-53 118,-53 118,-93 118,-93 118,-99 112,-105 106,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"30\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">load_data</text>\n",
       "<text text-anchor=\"start\" x=\"55.5\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;y -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;y</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M118.27,-88.5C131.11,-92.58 144.83,-96.94 157.23,-100.88\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"156.25,-104.25 166.84,-103.94 158.37,-97.57 156.25,-104.25\"/>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;X -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;X</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M118.27,-57.5C125.33,-55.25 132.66,-52.92 139.9,-50.63\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"141.19,-53.89 149.66,-47.52 139.07,-47.22 141.19,-53.89\"/>\n",
       "</g>\n",
       "<!-- function -->\n",
       "<g id=\"node6\" class=\"node\">\n",
       "<title>function</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M96.5,-217.5C96.5,-217.5 40.5,-217.5 40.5,-217.5 34.5,-217.5 28.5,-211.5 28.5,-205.5 28.5,-205.5 28.5,-192.5 28.5,-192.5 28.5,-186.5 34.5,-180.5 40.5,-180.5 40.5,-180.5 96.5,-180.5 96.5,-180.5 102.5,-180.5 108.5,-186.5 108.5,-192.5 108.5,-192.5 108.5,-205.5 108.5,-205.5 108.5,-211.5 102.5,-217.5 96.5,-217.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"68.5\" y=\"-195.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">function</text>\n",
       "</g>\n",
       "<!-- materializer -->\n",
       "<g id=\"node7\" class=\"node\">\n",
       "<title>materializer</title>\n",
       "<path fill=\"#ffffff\" stroke=\"black\" d=\"M121,-159.26C121,-161.26 97.47,-162.88 68.5,-162.88 39.53,-162.88 16,-161.26 16,-159.26 16,-159.26 16,-126.74 16,-126.74 16,-124.74 39.53,-123.12 68.5,-123.12 97.47,-123.12 121,-124.74 121,-126.74 121,-126.74 121,-159.26 121,-159.26\"/>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M121,-159.26C121,-157.27 97.47,-155.65 68.5,-155.65 39.53,-155.65 16,-157.27 16,-159.26\"/>\n",
       "<text text-anchor=\"middle\" x=\"68.5\" y=\"-139.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">materializer</text>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<hamilton.driver.Driver at 0x7f89dc70cf90>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from hamilton import driver\n",
    "from hamilton.io.materialization import to\n",
    "\n",
    "model_saver = to.mlflow(\n",
    "    id=\"trained_model__mlflow\",  # name given to the saver\n",
    "    dependencies=[\"trained_model\"],  # node returning the model\n",
    "    register_as=\"my_predictor\",  # name of the model in the MLFlow registry\n",
    ")\n",
    "\n",
    "dr = (\n",
    "    driver.Builder()\n",
    "    .with_modules(model_training)\n",
    "    .with_materializers(model_saver)\n",
    "    .build()\n",
    ")\n",
    "dr"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[0;31mInit signature:\u001b[0m\n",
      "\u001b[0mMLFlowModelSaver\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mpath\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpathlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'model'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mregister_as\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mflavor\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mrun_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mkwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mDict\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mAny\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mDocstring:\u001b[0m     \n",
      "Save model to the MLFlow tracking server using `.log_model()`\n",
      "\n",
      ":param path: Run relative path to store model. Will constitute the model URI.\n",
      ":param register_as: If not None, register the model under the specified name.\n",
      ":param flavor: Library format to save the model (sklearn, xgboost, etc.). Automatically inferred if None.\n",
      ":param run_id: Log model to a specific run. Leave to `None` if using the `MLFlowTracker`\n",
      ":param kwargs: Arguments for `.log_model()`. Can be flavor-specific.\n",
      "\u001b[0;31mFile:\u001b[0m           ~/projects/dagworks/hamilton/hamilton/plugins/mlflow_extensions.py\n",
      "\u001b[0;31mType:\u001b[0m           ABCMeta\n",
      "\u001b[0;31mSubclasses:\u001b[0m     "
     ]
    }
   ],
   "source": [
    "# see the full API\n",
    "from hamilton.plugins.mlflow_extensions import MLFlowModelSaver\n",
    "MLFlowModelSaver?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.3 Execute\n",
    "We execute our dataflow by calling `Driver.execute()` and requesting node names. Requesting `trained_model` will train the model and return it. Requesting `trained_model__mlflow` will train the model, save it, and return metadata. We then visualize the execution path "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Registered model 'my_predictor' already exists. Creating a new version of this model...\n",
      "Created version '8' of model 'my_predictor'.\n"
     ]
    },
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"667pt\" height=\"320pt\"\n",
       " viewBox=\"0.00 0.00 667.00 320.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 316)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-316 663,-316 663,4 -4,4\"/>\n",
       "<g id=\"clust1\" class=\"cluster\">\n",
       "<title>cluster__legend</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" points=\"8,-115 8,-304 129,-304 129,-115 8,-115\"/>\n",
       "<text text-anchor=\"middle\" x=\"68.5\" y=\"-288.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">Legend</text>\n",
       "</g>\n",
       "<!-- X -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>X</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M237,-146C237,-146 162,-146 162,-146 156,-146 150,-140 150,-134 150,-134 150,-94 150,-94 150,-88 156,-82 162,-82 162,-82 237,-82 237,-82 243,-82 249,-88 249,-94 249,-94 249,-134 249,-134 249,-140 243,-146 237,-146\"/>\n",
       "<text text-anchor=\"start\" x=\"194\" y=\"-124.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">X</text>\n",
       "<text text-anchor=\"start\" x=\"161\" y=\"-96.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- trained_model -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>trained_model</title>\n",
       "<path fill=\"#ffc857\" stroke=\"black\" d=\"M418,-105C418,-105 290,-105 290,-105 284,-105 278,-99 278,-93 278,-93 278,-53 278,-53 278,-47 284,-41 290,-41 290,-41 418,-41 418,-41 424,-41 430,-47 430,-53 430,-53 430,-93 430,-93 430,-99 424,-105 418,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"298\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">trained_model</text>\n",
       "<text text-anchor=\"start\" x=\"289\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">LogisticRegression</text>\n",
       "</g>\n",
       "<!-- X&#45;&gt;trained_model -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>X&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M249.17,-100.91C255.18,-99.3 261.45,-97.61 267.8,-95.91\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"269.08,-99.19 277.83,-93.21 267.27,-92.43 269.08,-99.19\"/>\n",
       "</g>\n",
       "<!-- trained_model__mlflow -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>trained_model__mlflow</title>\n",
       "<path fill=\"#ffc857\" stroke=\"black\" d=\"M659,-109C659,-113.41 614.18,-117 559,-117 503.82,-117 459,-113.41 459,-109 459,-109 459,-37 459,-37 459,-32.59 503.82,-29 559,-29 614.18,-29 659,-32.59 659,-37 659,-37 659,-109 659,-109\"/>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M659,-109C659,-104.59 614.18,-101 559,-101 503.82,-101 459,-104.59 459,-109\"/>\n",
       "<text text-anchor=\"start\" x=\"470\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">trained_model__mlflow</text>\n",
       "<text text-anchor=\"start\" x=\"492.5\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">MLFlowModelSaver</text>\n",
       "</g>\n",
       "<!-- trained_model&#45;&gt;trained_model__mlflow -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>trained_model&#45;&gt;trained_model__mlflow</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M430.01,-73C436.02,-73 442.16,-73 448.34,-73\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"448.76,-76.5 458.76,-73 448.76,-69.5 448.76,-76.5\"/>\n",
       "</g>\n",
       "<!-- load_data -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>load_data</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M106,-105C106,-105 31,-105 31,-105 25,-105 19,-99 19,-93 19,-93 19,-53 19,-53 19,-47 25,-41 31,-41 31,-41 106,-41 106,-41 112,-41 118,-47 118,-53 118,-53 118,-93 118,-93 118,-99 112,-105 106,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"30\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">load_data</text>\n",
       "<text text-anchor=\"start\" x=\"55.5\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;X -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;X</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M118.27,-88.5C125.33,-90.75 132.66,-93.08 139.9,-95.37\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"139.07,-98.78 149.66,-98.48 141.19,-92.11 139.07,-98.78\"/>\n",
       "</g>\n",
       "<!-- y -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>y</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M220,-64C220,-64 179,-64 179,-64 173,-64 167,-58 167,-52 167,-52 167,-12 167,-12 167,-6 173,0 179,0 179,0 220,0 220,0 226,0 232,-6 232,-12 232,-12 232,-52 232,-52 232,-58 226,-64 220,-64\"/>\n",
       "<text text-anchor=\"start\" x=\"194.5\" y=\"-42.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">y</text>\n",
       "<text text-anchor=\"start\" x=\"178\" y=\"-14.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;y -->\n",
       "<g id=\"edge5\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;y</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M118.27,-57.5C131.11,-53.42 144.83,-49.06 157.23,-45.12\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"158.37,-48.43 166.84,-42.06 156.25,-41.75 158.37,-48.43\"/>\n",
       "</g>\n",
       "<!-- y&#45;&gt;trained_model -->\n",
       "<g id=\"edge4\" class=\"edge\">\n",
       "<title>y&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M232.1,-40.5C242.73,-43.35 255.16,-46.7 267.92,-50.13\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"267.16,-53.54 277.72,-52.76 268.97,-46.78 267.16,-53.54\"/>\n",
       "</g>\n",
       "<!-- function -->\n",
       "<g id=\"node6\" class=\"node\">\n",
       "<title>function</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M96.5,-272.5C96.5,-272.5 40.5,-272.5 40.5,-272.5 34.5,-272.5 28.5,-266.5 28.5,-260.5 28.5,-260.5 28.5,-247.5 28.5,-247.5 28.5,-241.5 34.5,-235.5 40.5,-235.5 40.5,-235.5 96.5,-235.5 96.5,-235.5 102.5,-235.5 108.5,-241.5 108.5,-247.5 108.5,-247.5 108.5,-260.5 108.5,-260.5 108.5,-266.5 102.5,-272.5 96.5,-272.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"68.5\" y=\"-250.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">function</text>\n",
       "</g>\n",
       "<!-- output -->\n",
       "<g id=\"node7\" class=\"node\">\n",
       "<title>output</title>\n",
       "<path fill=\"#ffc857\" stroke=\"black\" d=\"M90.5,-217.5C90.5,-217.5 46.5,-217.5 46.5,-217.5 40.5,-217.5 34.5,-211.5 34.5,-205.5 34.5,-205.5 34.5,-192.5 34.5,-192.5 34.5,-186.5 40.5,-180.5 46.5,-180.5 46.5,-180.5 90.5,-180.5 90.5,-180.5 96.5,-180.5 102.5,-186.5 102.5,-192.5 102.5,-192.5 102.5,-205.5 102.5,-205.5 102.5,-211.5 96.5,-217.5 90.5,-217.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"68.5\" y=\"-195.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">output</text>\n",
       "</g>\n",
       "<!-- materializer -->\n",
       "<g id=\"node8\" class=\"node\">\n",
       "<title>materializer</title>\n",
       "<path fill=\"#ffffff\" stroke=\"black\" d=\"M121,-159.26C121,-161.26 97.47,-162.88 68.5,-162.88 39.53,-162.88 16,-161.26 16,-159.26 16,-159.26 16,-126.74 16,-126.74 16,-124.74 39.53,-123.12 68.5,-123.12 97.47,-123.12 121,-124.74 121,-126.74 121,-126.74 121,-159.26 121,-159.26\"/>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M121,-159.26C121,-157.27 97.47,-155.65 68.5,-155.65 39.53,-155.65 16,-157.27 16,-159.26\"/>\n",
       "<text text-anchor=\"middle\" x=\"68.5\" y=\"-139.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">materializer</text>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<graphviz.graphs.Digraph at 0x7f89d9180550>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "results = dr.execute([\"trained_model\", \"trained_model__mlflow\"])\n",
    "dr.visualize_execution([\"trained_model\", \"trained_model__mlflow\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'artifact_path': 'model',\n",
       " 'flavors': {'python_function': {'model_path': 'model.pkl',\n",
       "   'predict_fn': 'predict',\n",
       "   'loader_module': 'mlflow.sklearn',\n",
       "   'python_version': '3.11.1',\n",
       "   'env': {'conda': 'conda.yaml', 'virtualenv': 'python_env.yaml'}},\n",
       "  'sklearn': {'pickled_model': 'model.pkl',\n",
       "   'sklearn_version': '1.5.0',\n",
       "   'serialization_format': 'cloudpickle',\n",
       "   'code': None}},\n",
       " 'model_uri': 'runs:/30c106f0915d43fda9f5974fb36cdc39/model',\n",
       " 'model_uuid': '860cc1ea405d461d8c58db4b68d25158',\n",
       " 'run_id': '30c106f0915d43fda9f5974fb36cdc39',\n",
       " 'saved_input_example_info': None,\n",
       " 'signature_dict': None,\n",
       " 'signature': None,\n",
       " 'utc_time_created': '2024-06-11 15:01:30.896479',\n",
       " 'mlflow_version': '2.13.2',\n",
       " 'metadata': None,\n",
       " 'registered_model': {'name': 'my_predictor',\n",
       "  'version': 8,\n",
       "  'creation_time': 1718118092290,\n",
       "  'last_updated_timestamp': 1718118092290,\n",
       "  'description': None,\n",
       "  'user_id': None,\n",
       "  'current_stage': 'None',\n",
       "  'source': 'file:///home/tjean/projects/dagworks/hamilton/examples/mlflow/mlruns/0/30c106f0915d43fda9f5974fb36cdc39/artifacts/model',\n",
       "  'run_id': '30c106f0915d43fda9f5974fb36cdc39',\n",
       "  'run_link': None,\n",
       "  'status': 'READY',\n",
       "  'status_message': None,\n",
       "  'tags': {},\n",
       "  'aliases': []}}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# we can inspect the model metadata\n",
    "results[\"trained_model__mlflow\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Model Inference Dataflow"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.1 Define\n",
    "We define a simple dataflow that uses a trained model to make predictions on user inputs. Parameters that point to no other functions (e.g, `user_input`) are called \"inputs\" as you see on the visualization.\n",
    "\n",
    "We annotate `model: BaseEstimator` to allow any scikit-learn model to be passed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"489pt\" height=\"276pt\"\n",
       " viewBox=\"0.00 0.00 489.00 275.50\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 271.5)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-271.5 485,-271.5 485,4 -4,4\"/>\n",
       "<g id=\"clust1\" class=\"cluster\">\n",
       "<title>cluster__legend</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" points=\"20,-127.5 20,-259.5 116,-259.5 116,-127.5 20,-127.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"68\" y=\"-244.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">Legend</text>\n",
       "</g>\n",
       "<!-- prediction -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>prediction</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M469,-90.5C469,-90.5 391,-90.5 391,-90.5 385,-90.5 379,-84.5 379,-78.5 379,-78.5 379,-38.5 379,-38.5 379,-32.5 385,-26.5 391,-26.5 391,-26.5 469,-26.5 469,-26.5 475,-26.5 481,-32.5 481,-38.5 481,-38.5 481,-78.5 481,-78.5 481,-84.5 475,-90.5 469,-90.5\"/>\n",
       "<text text-anchor=\"start\" x=\"390\" y=\"-69.3\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">prediction</text>\n",
       "<text text-anchor=\"start\" x=\"420.5\" y=\"-41.3\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">int</text>\n",
       "</g>\n",
       "<!-- preprocessed_inputs -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>preprocessed_inputs</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M338,-127.5C338,-127.5 177,-127.5 177,-127.5 171,-127.5 165,-121.5 165,-115.5 165,-115.5 165,-75.5 165,-75.5 165,-69.5 171,-63.5 177,-63.5 177,-63.5 338,-63.5 338,-63.5 344,-63.5 350,-69.5 350,-75.5 350,-75.5 350,-115.5 350,-115.5 350,-121.5 344,-127.5 338,-127.5\"/>\n",
       "<text text-anchor=\"start\" x=\"176\" y=\"-106.3\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">preprocessed_inputs</text>\n",
       "<text text-anchor=\"start\" x=\"219\" y=\"-78.3\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- preprocessed_inputs&#45;&gt;prediction -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>preprocessed_inputs&#45;&gt;prediction</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M350.24,-75.59C356.5,-74.23 362.71,-72.88 368.74,-71.58\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"369.67,-74.96 378.7,-69.42 368.18,-68.12 369.67,-74.96\"/>\n",
       "</g>\n",
       "<!-- _prediction_inputs -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>_prediction_inputs</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"347.5,-45 167.5,-45 167.5,0 347.5,0 347.5,-45\"/>\n",
       "<text text-anchor=\"start\" x=\"182.5\" y=\"-18.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">model</text>\n",
       "<text text-anchor=\"start\" x=\"231.5\" y=\"-18.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">BaseEstimator</text>\n",
       "</g>\n",
       "<!-- _prediction_inputs&#45;&gt;prediction -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>_prediction_inputs&#45;&gt;prediction</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M347.74,-41.34C354.89,-42.85 362,-44.35 368.87,-45.8\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"368.23,-49.25 378.74,-47.89 369.68,-42.4 368.23,-49.25\"/>\n",
       "</g>\n",
       "<!-- _preprocessed_inputs_inputs -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>_preprocessed_inputs_inputs</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"136,-118 0,-118 0,-73 136,-73 136,-118\"/>\n",
       "<text text-anchor=\"start\" x=\"15\" y=\"-91.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">user_input</text>\n",
       "<text text-anchor=\"start\" x=\"95\" y=\"-91.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- _preprocessed_inputs_inputs&#45;&gt;preprocessed_inputs -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>_preprocessed_inputs_inputs&#45;&gt;preprocessed_inputs</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M136.16,-95.5C142.3,-95.5 148.6,-95.5 154.95,-95.5\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"154.96,-99 164.96,-95.5 154.96,-92 154.96,-99\"/>\n",
       "</g>\n",
       "<!-- input -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>input</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"97.5,-228 38.5,-228 38.5,-191 97.5,-191 97.5,-228\"/>\n",
       "<text text-anchor=\"middle\" x=\"68\" y=\"-205.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">input</text>\n",
       "</g>\n",
       "<!-- function -->\n",
       "<g id=\"node6\" class=\"node\">\n",
       "<title>function</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M96,-173C96,-173 40,-173 40,-173 34,-173 28,-167 28,-161 28,-161 28,-148 28,-148 28,-142 34,-136 40,-136 40,-136 96,-136 96,-136 102,-136 108,-142 108,-148 108,-148 108,-161 108,-161 108,-167 102,-173 96,-173\"/>\n",
       "<text text-anchor=\"middle\" x=\"68\" y=\"-150.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">function</text>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<graphviz.graphs.Digraph at 0x7f89d9104410>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%%cell_to_module model_inference --display\n",
    "import pandas as pd\n",
    "from sklearn.base import BaseEstimator\n",
    "\n",
    "def preprocessed_inputs(user_input: dict) -> pd.DataFrame:\n",
    "    df = pd.DataFrame(user_input, index=[0])\n",
    "    return df\n",
    "\n",
    "def prediction(preprocessed_inputs: pd.DataFrame, model: BaseEstimator) -> int:\n",
    "    return model.predict(preprocessed_inputs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 Assemble\n",
    "Again, we create a `Driver`, but this time we use `from_.mlflow()` to create a `MLFlowModelLoader` that looks for model `my_predictor` in the MLFlow registry. We pass this object through `.with_materializers()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"505pt\" height=\"294pt\"\n",
       " viewBox=\"0.00 0.00 505.00 294.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 290)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-290 501,-290 501,4 -4,4\"/>\n",
       "<g id=\"clust1\" class=\"cluster\">\n",
       "<title>cluster__legend</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" points=\"28,-146 28,-278 124,-278 124,-146 28,-146\"/>\n",
       "<text text-anchor=\"middle\" x=\"76\" y=\"-262.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">Legend</text>\n",
       "</g>\n",
       "<!-- model -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>model</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M323,-64C323,-64 224,-64 224,-64 218,-64 212,-58 212,-52 212,-52 212,-12 212,-12 212,-6 218,0 224,0 224,0 323,0 323,0 329,0 335,-6 335,-12 335,-12 335,-52 335,-52 335,-58 329,-64 323,-64\"/>\n",
       "<text text-anchor=\"start\" x=\"249\" y=\"-42.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">model</text>\n",
       "<text text-anchor=\"start\" x=\"223\" y=\"-14.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">BaseEstimator</text>\n",
       "</g>\n",
       "<!-- prediction -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>prediction</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M485,-105C485,-105 407,-105 407,-105 401,-105 395,-99 395,-93 395,-93 395,-53 395,-53 395,-47 401,-41 407,-41 407,-41 485,-41 485,-41 491,-41 497,-47 497,-53 497,-53 497,-93 497,-93 497,-99 491,-105 485,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"406\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">prediction</text>\n",
       "<text text-anchor=\"start\" x=\"436.5\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">int</text>\n",
       "</g>\n",
       "<!-- model&#45;&gt;prediction -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>model&#45;&gt;prediction</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M335.09,-46.57C351.26,-50.46 368.73,-54.66 384.87,-58.54\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"384.19,-61.98 394.73,-60.91 385.83,-55.17 384.19,-61.98\"/>\n",
       "</g>\n",
       "<!-- preprocessed_inputs -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>preprocessed_inputs</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M354,-146C354,-146 193,-146 193,-146 187,-146 181,-140 181,-134 181,-134 181,-94 181,-94 181,-88 187,-82 193,-82 193,-82 354,-82 354,-82 360,-82 366,-88 366,-94 366,-94 366,-134 366,-134 366,-140 360,-146 354,-146\"/>\n",
       "<text text-anchor=\"start\" x=\"192\" y=\"-124.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">preprocessed_inputs</text>\n",
       "<text text-anchor=\"start\" x=\"235\" y=\"-96.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- preprocessed_inputs&#45;&gt;prediction -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>preprocessed_inputs&#45;&gt;prediction</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M366.24,-91.94C372.5,-90.43 378.71,-88.94 384.74,-87.49\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"385.79,-90.84 394.7,-85.1 384.16,-84.03 385.79,-90.84\"/>\n",
       "</g>\n",
       "<!-- load_data.model -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>load_data.model</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M140,-64C140,-64 12,-64 12,-64 6,-64 0,-58 0,-52 0,-52 0,-12 0,-12 0,-6 6,0 12,0 12,0 140,0 140,0 146,0 152,-6 152,-12 152,-12 152,-52 152,-52 152,-58 146,-64 140,-64\"/>\n",
       "<text text-anchor=\"start\" x=\"11\" y=\"-42.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">load_data.model</text>\n",
       "<text text-anchor=\"start\" x=\"57\" y=\"-14.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Tuple</text>\n",
       "</g>\n",
       "<!-- load_data.model&#45;&gt;model -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>load_data.model&#45;&gt;model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M152.03,-32C168.28,-32 185.42,-32 201.51,-32\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"201.91,-35.5 211.91,-32 201.91,-28.5 201.91,-35.5\"/>\n",
       "</g>\n",
       "<!-- _preprocessed_inputs_inputs -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>_preprocessed_inputs_inputs</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"144,-136.5 8,-136.5 8,-91.5 144,-91.5 144,-136.5\"/>\n",
       "<text text-anchor=\"start\" x=\"23\" y=\"-109.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">user_input</text>\n",
       "<text text-anchor=\"start\" x=\"103\" y=\"-109.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- _preprocessed_inputs_inputs&#45;&gt;preprocessed_inputs -->\n",
       "<g id=\"edge4\" class=\"edge\">\n",
       "<title>_preprocessed_inputs_inputs&#45;&gt;preprocessed_inputs</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M144.01,-114C152.6,-114 161.55,-114 170.54,-114\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"170.7,-117.5 180.7,-114 170.7,-110.5 170.7,-117.5\"/>\n",
       "</g>\n",
       "<!-- input -->\n",
       "<g id=\"node6\" class=\"node\">\n",
       "<title>input</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"105.5,-246.5 46.5,-246.5 46.5,-209.5 105.5,-209.5 105.5,-246.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"76\" y=\"-224.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">input</text>\n",
       "</g>\n",
       "<!-- function -->\n",
       "<g id=\"node7\" class=\"node\">\n",
       "<title>function</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M104,-191.5C104,-191.5 48,-191.5 48,-191.5 42,-191.5 36,-185.5 36,-179.5 36,-179.5 36,-166.5 36,-166.5 36,-160.5 42,-154.5 48,-154.5 48,-154.5 104,-154.5 104,-154.5 110,-154.5 116,-160.5 116,-166.5 116,-166.5 116,-179.5 116,-179.5 116,-185.5 110,-191.5 104,-191.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"76\" y=\"-169.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">function</text>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<hamilton.driver.Driver at 0x7f89d90880d0>"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from hamilton import driver\n",
    "from hamilton.io.materialization import from_\n",
    "\n",
    "model_loader = from_.mlflow(\n",
    "    target=\"model\",\n",
    "    mode=\"registry\",\n",
    "    model_name=\"my_predictor\",\n",
    "    version=1,\n",
    ")\n",
    "\n",
    "dr = (\n",
    "    driver.Builder()\n",
    "    .with_modules(model_inference)\n",
    "    .with_materializers(model_loader)\n",
    "    .build()\n",
    ")\n",
    "dr"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[0;31mInit signature:\u001b[0m\n",
      "\u001b[0mMLFlowModelLoader\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mmodel_uri\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mmode\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mLiteral\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'tracking'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'registry'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'tracking'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mrun_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mpath\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpathlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'model'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mmodel_name\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mversion\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mint\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mNoneType\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mversion_alias\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mflavor\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mkwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mDict\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mAny\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mDocstring:\u001b[0m     \n",
      "Load model from the MLFlow tracking server or model registry using .load_model()\n",
      "You can pass a model URI or the necessary metadata to retrieve the model\n",
      "\n",
      ":param model_uri: Model location starting as `runs:/` for tracking or `models:/` for registry\n",
      ":param mode: `tracking` or registry`. tracking needs `run_id` and `path`. registry needs `model_name` and `version` or `version_alias`.\n",
      ":param run_id: Run id of the model on the tracking server\n",
      ":param path: Run relative path where the model is stored\n",
      ":param model_name: Name of the registered model (equivalent to `register_as` in model saver)\n",
      ":param version: Version of the registered model. Can pass as string `v1` or integer `1`\n",
      ":param version_alias: Version alias of the registered model. Specify either this or `version`\n",
      ":param flavor: Library format to load the model (sklearn, xgboost, etc.). Automatically inferred if None.\n",
      ":param kwargs: Arguments for `.load_model()`. Can be flavor-specific.\n",
      "\u001b[0;31mFile:\u001b[0m           ~/projects/dagworks/hamilton/hamilton/plugins/mlflow_extensions.py\n",
      "\u001b[0;31mType:\u001b[0m           ABCMeta\n",
      "\u001b[0;31mSubclasses:\u001b[0m     "
     ]
    }
   ],
   "source": [
    "# see the full API\n",
    "from hamilton.plugins.mlflow_extensions import MLFlowModelLoader\n",
    "MLFlowModelLoader?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.3 Execute\n",
    "We simulate user inputs that match the `fare` and `age` columns of the training data. Then, we request `prediction` and `load_data.model` to return the loaded model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"505pt\" height=\"359pt\"\n",
       " viewBox=\"0.00 0.00 505.00 359.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 355)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-355 501,-355 501,4 -4,4\"/>\n",
       "<g id=\"clust1\" class=\"cluster\">\n",
       "<title>cluster__legend</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" points=\"28,-156 28,-343 124,-343 124,-156 28,-156\"/>\n",
       "<text text-anchor=\"middle\" x=\"76\" y=\"-327.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">Legend</text>\n",
       "</g>\n",
       "<!-- prediction -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>prediction</title>\n",
       "<path fill=\"#ffc857\" stroke=\"black\" d=\"M485,-105C485,-105 407,-105 407,-105 401,-105 395,-99 395,-93 395,-93 395,-53 395,-53 395,-47 401,-41 407,-41 407,-41 485,-41 485,-41 491,-41 497,-47 497,-53 497,-53 497,-93 497,-93 497,-99 491,-105 485,-105\"/>\n",
       "<text text-anchor=\"start\" x=\"406\" y=\"-83.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">prediction</text>\n",
       "<text text-anchor=\"start\" x=\"436.5\" y=\"-55.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">int</text>\n",
       "</g>\n",
       "<!-- model -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>model</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M323,-146C323,-146 224,-146 224,-146 218,-146 212,-140 212,-134 212,-134 212,-94 212,-94 212,-88 218,-82 224,-82 224,-82 323,-82 323,-82 329,-82 335,-88 335,-94 335,-94 335,-134 335,-134 335,-140 329,-146 323,-146\"/>\n",
       "<text text-anchor=\"start\" x=\"249\" y=\"-124.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">model</text>\n",
       "<text text-anchor=\"start\" x=\"223\" y=\"-96.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">BaseEstimator</text>\n",
       "</g>\n",
       "<!-- model&#45;&gt;prediction -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>model&#45;&gt;prediction</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M335.09,-99.43C351.26,-95.54 368.73,-91.34 384.87,-87.46\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"385.83,-90.83 394.73,-85.09 384.19,-84.02 385.83,-90.83\"/>\n",
       "</g>\n",
       "<!-- preprocessed_inputs -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>preprocessed_inputs</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M354,-64C354,-64 193,-64 193,-64 187,-64 181,-58 181,-52 181,-52 181,-12 181,-12 181,-6 187,0 193,0 193,0 354,0 354,0 360,0 366,-6 366,-12 366,-12 366,-52 366,-52 366,-58 360,-64 354,-64\"/>\n",
       "<text text-anchor=\"start\" x=\"192\" y=\"-42.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">preprocessed_inputs</text>\n",
       "<text text-anchor=\"start\" x=\"235\" y=\"-14.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- preprocessed_inputs&#45;&gt;prediction -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>preprocessed_inputs&#45;&gt;prediction</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M366.24,-54.06C372.5,-55.57 378.71,-57.06 384.74,-58.51\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"384.16,-61.97 394.7,-60.9 385.79,-55.16 384.16,-61.97\"/>\n",
       "</g>\n",
       "<!-- load_data.model -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>load_data.model</title>\n",
       "<path fill=\"#ffc857\" stroke=\"black\" d=\"M140,-146C140,-146 12,-146 12,-146 6,-146 0,-140 0,-134 0,-134 0,-94 0,-94 0,-88 6,-82 12,-82 12,-82 140,-82 140,-82 146,-82 152,-88 152,-94 152,-94 152,-134 152,-134 152,-140 146,-146 140,-146\"/>\n",
       "<text text-anchor=\"start\" x=\"11\" y=\"-124.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">load_data.model</text>\n",
       "<text text-anchor=\"start\" x=\"57\" y=\"-96.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Tuple</text>\n",
       "</g>\n",
       "<!-- load_data.model&#45;&gt;model -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>load_data.model&#45;&gt;model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M152.03,-114C168.28,-114 185.42,-114 201.51,-114\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"201.91,-117.5 211.91,-114 201.91,-110.5 201.91,-117.5\"/>\n",
       "</g>\n",
       "<!-- _preprocessed_inputs_inputs -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>_preprocessed_inputs_inputs</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"144,-54.5 8,-54.5 8,-9.5 144,-9.5 144,-54.5\"/>\n",
       "<text text-anchor=\"start\" x=\"23\" y=\"-27.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">user_input</text>\n",
       "<text text-anchor=\"start\" x=\"103\" y=\"-27.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- _preprocessed_inputs_inputs&#45;&gt;preprocessed_inputs -->\n",
       "<g id=\"edge4\" class=\"edge\">\n",
       "<title>_preprocessed_inputs_inputs&#45;&gt;preprocessed_inputs</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M144.01,-32C152.6,-32 161.55,-32 170.54,-32\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"170.7,-35.5 180.7,-32 170.7,-28.5 170.7,-35.5\"/>\n",
       "</g>\n",
       "<!-- input -->\n",
       "<g id=\"node6\" class=\"node\">\n",
       "<title>input</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"105.5,-311.5 46.5,-311.5 46.5,-274.5 105.5,-274.5 105.5,-311.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"76\" y=\"-289.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">input</text>\n",
       "</g>\n",
       "<!-- function -->\n",
       "<g id=\"node7\" class=\"node\">\n",
       "<title>function</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M104,-256.5C104,-256.5 48,-256.5 48,-256.5 42,-256.5 36,-250.5 36,-244.5 36,-244.5 36,-231.5 36,-231.5 36,-225.5 42,-219.5 48,-219.5 48,-219.5 104,-219.5 104,-219.5 110,-219.5 116,-225.5 116,-231.5 116,-231.5 116,-244.5 116,-244.5 116,-250.5 110,-256.5 104,-256.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"76\" y=\"-234.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">function</text>\n",
       "</g>\n",
       "<!-- output -->\n",
       "<g id=\"node8\" class=\"node\">\n",
       "<title>output</title>\n",
       "<path fill=\"#ffc857\" stroke=\"black\" d=\"M98,-201.5C98,-201.5 54,-201.5 54,-201.5 48,-201.5 42,-195.5 42,-189.5 42,-189.5 42,-176.5 42,-176.5 42,-170.5 48,-164.5 54,-164.5 54,-164.5 98,-164.5 98,-164.5 104,-164.5 110,-170.5 110,-176.5 110,-176.5 110,-189.5 110,-189.5 110,-195.5 104,-201.5 98,-201.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"76\" y=\"-179.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">output</text>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<graphviz.graphs.Digraph at 0x7f89d90ebd90>"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "inputs = dict(user_input={\"fare\": 10.72, \"age\": 48})\n",
    "\n",
    "results = dr.execute([\"prediction\", \"load_data.model\"], inputs=inputs)\n",
    "dr.visualize_execution([\"prediction\", \"load_data.model\"], inputs=inputs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['0']\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "(LogisticRegression(),\n",
       " {'artifact_path': 'model',\n",
       "  'flavors': {'python_function': {'env': {'conda': 'conda.yaml',\n",
       "     'virtualenv': 'python_env.yaml'},\n",
       "    'loader_module': 'mlflow.sklearn',\n",
       "    'model_path': 'model.pkl',\n",
       "    'predict_fn': 'predict',\n",
       "    'python_version': '3.11.1'},\n",
       "   'sklearn': {'code': None,\n",
       "    'pickled_model': 'model.pkl',\n",
       "    'serialization_format': 'cloudpickle',\n",
       "    'sklearn_version': '1.5.0'}},\n",
       "  'model_uri': 'models:/my_predictor/1',\n",
       "  'model_uuid': '5cfda7f11ed440e6823c13a99dc47471',\n",
       "  'run_id': '8099b8e575d04476b47960431d17f9f5',\n",
       "  'saved_input_example_info': None,\n",
       "  'signature_dict': None,\n",
       "  'signature': None,\n",
       "  'utc_time_created': '2024-06-10 22:43:07.254057',\n",
       "  'mlflow_version': '2.13.2',\n",
       "  'metadata': None})"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# we can inspect the prediction\n",
    "# load_data.model returns a tuple (model, model metadata)\n",
    "print(results[\"prediction\"])\n",
    "results[\"load_data.model\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. MLFlowTracker\n",
    "So far, we saved and loaded models, but the MLFlow metadata is almost empty. By adding the `MLFlowTracker()`, we can automatically track run configurations, metrics, figures, and other artifacts."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.1 Define\n",
    "We define a slightly more complex pipeline that splits the dataset into training and test sets. Then, we compute the model performance on each set and produce a scatter plot of features and correct/incorrect predictions.\n",
    "\n",
    "Notice that no `mlflow` statements is needed in our dataflow definition."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"1155pt\" height=\"353pt\"\n",
       " viewBox=\"0.00 0.00 1154.50 353.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 349)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-349 1150.5,-349 1150.5,4 -4,4\"/>\n",
       "<g id=\"clust1\" class=\"cluster\">\n",
       "<title>cluster__legend</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" points=\"8,-205 8,-337 104,-337 104,-205 8,-205\"/>\n",
       "<text text-anchor=\"middle\" x=\"56\" y=\"-321.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">Legend</text>\n",
       "</g>\n",
       "<!-- y_train -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>y_train</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M580,-72C580,-72 528,-72 528,-72 522,-72 516,-66 516,-60 516,-60 516,-20 516,-20 516,-14 522,-8 528,-8 528,-8 580,-8 580,-8 586,-8 592,-14 592,-20 592,-20 592,-60 592,-60 592,-66 586,-72 580,-72\"/>\n",
       "<text text-anchor=\"start\" x=\"527\" y=\"-50.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">y_train</text>\n",
       "<text text-anchor=\"start\" x=\"532.5\" y=\"-22.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- trained_model -->\n",
       "<g id=\"node10\" class=\"node\">\n",
       "<title>trained_model</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M754.5,-94C754.5,-94 644.5,-94 644.5,-94 638.5,-94 632.5,-88 632.5,-82 632.5,-82 632.5,-42 632.5,-42 632.5,-36 638.5,-30 644.5,-30 644.5,-30 754.5,-30 754.5,-30 760.5,-30 766.5,-36 766.5,-42 766.5,-42 766.5,-82 766.5,-82 766.5,-88 760.5,-94 754.5,-94\"/>\n",
       "<text text-anchor=\"start\" x=\"643.5\" y=\"-72.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">trained_model</text>\n",
       "<text text-anchor=\"start\" x=\"649\" y=\"-44.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">BaseEstimator</text>\n",
       "</g>\n",
       "<!-- y_train&#45;&gt;trained_model -->\n",
       "<g id=\"edge16\" class=\"edge\">\n",
       "<title>y_train&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M592.33,-45.72C601.64,-47.15 611.97,-48.73 622.42,-50.34\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"622.03,-53.82 632.45,-51.87 623.1,-46.9 622.03,-53.82\"/>\n",
       "</g>\n",
       "<!-- train_performance -->\n",
       "<g id=\"node14\" class=\"node\">\n",
       "<title>train_performance</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M1134.5,-64C1134.5,-64 991.5,-64 991.5,-64 985.5,-64 979.5,-58 979.5,-52 979.5,-52 979.5,-12 979.5,-12 979.5,-6 985.5,0 991.5,0 991.5,0 1134.5,0 1134.5,0 1140.5,0 1146.5,-6 1146.5,-12 1146.5,-12 1146.5,-52 1146.5,-52 1146.5,-58 1140.5,-64 1134.5,-64\"/>\n",
       "<text text-anchor=\"start\" x=\"990.5\" y=\"-42.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">train_performance</text>\n",
       "<text text-anchor=\"start\" x=\"1047.5\" y=\"-14.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">float</text>\n",
       "</g>\n",
       "<!-- y_train&#45;&gt;train_performance -->\n",
       "<g id=\"edge21\" class=\"edge\">\n",
       "<title>y_train&#45;&gt;train_performance</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M592.11,-28.98C604.8,-25.73 619.16,-22.61 632.5,-21 772.81,-4.05 809.34,-14.05 950.5,-21 956.62,-21.3 962.92,-21.69 969.27,-22.15\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"969.02,-25.64 979.26,-22.92 969.56,-18.67 969.02,-25.64\"/>\n",
       "</g>\n",
       "<!-- y_test -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>y_test</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M896,-258C896,-258 850,-258 850,-258 844,-258 838,-252 838,-246 838,-246 838,-206 838,-206 838,-200 844,-194 850,-194 850,-194 896,-194 896,-194 902,-194 908,-200 908,-206 908,-206 908,-246 908,-246 908,-252 902,-258 896,-258\"/>\n",
       "<text text-anchor=\"start\" x=\"849\" y=\"-236.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">y_test</text>\n",
       "<text text-anchor=\"start\" x=\"851.5\" y=\"-208.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- test_performance -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>test_performance</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M1131.5,-176C1131.5,-176 994.5,-176 994.5,-176 988.5,-176 982.5,-170 982.5,-164 982.5,-164 982.5,-124 982.5,-124 982.5,-118 988.5,-112 994.5,-112 994.5,-112 1131.5,-112 1131.5,-112 1137.5,-112 1143.5,-118 1143.5,-124 1143.5,-124 1143.5,-164 1143.5,-164 1143.5,-170 1137.5,-176 1131.5,-176\"/>\n",
       "<text text-anchor=\"start\" x=\"993.5\" y=\"-154.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">test_performance</text>\n",
       "<text text-anchor=\"start\" x=\"1047.5\" y=\"-126.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">float</text>\n",
       "</g>\n",
       "<!-- y_test&#45;&gt;test_performance -->\n",
       "<g id=\"edge4\" class=\"edge\">\n",
       "<title>y_test&#45;&gt;test_performance</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M908.05,-211.15C928.02,-202.44 954.2,-191.02 979.3,-180.07\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"980.71,-183.27 988.48,-176.07 977.91,-176.86 980.71,-183.27\"/>\n",
       "</g>\n",
       "<!-- test_scatter_plot -->\n",
       "<g id=\"node8\" class=\"node\">\n",
       "<title>test_scatter_plot</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M1128.5,-258C1128.5,-258 997.5,-258 997.5,-258 991.5,-258 985.5,-252 985.5,-246 985.5,-246 985.5,-206 985.5,-206 985.5,-200 991.5,-194 997.5,-194 997.5,-194 1128.5,-194 1128.5,-194 1134.5,-194 1140.5,-200 1140.5,-206 1140.5,-206 1140.5,-246 1140.5,-246 1140.5,-252 1134.5,-258 1128.5,-258\"/>\n",
       "<text text-anchor=\"start\" x=\"996.5\" y=\"-236.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">test_scatter_plot</text>\n",
       "<text text-anchor=\"start\" x=\"1040.5\" y=\"-208.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Figure</text>\n",
       "</g>\n",
       "<!-- y_test&#45;&gt;test_scatter_plot -->\n",
       "<g id=\"edge10\" class=\"edge\">\n",
       "<title>y_test&#45;&gt;test_scatter_plot</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M908.05,-226C926.93,-226 951.36,-226 975.18,-226\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"975.33,-229.5 985.33,-226 975.33,-222.5 975.33,-229.5\"/>\n",
       "</g>\n",
       "<!-- X_train -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>X_train</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M591.5,-154C591.5,-154 516.5,-154 516.5,-154 510.5,-154 504.5,-148 504.5,-142 504.5,-142 504.5,-102 504.5,-102 504.5,-96 510.5,-90 516.5,-90 516.5,-90 591.5,-90 591.5,-90 597.5,-90 603.5,-96 603.5,-102 603.5,-102 603.5,-142 603.5,-142 603.5,-148 597.5,-154 591.5,-154\"/>\n",
       "<text text-anchor=\"start\" x=\"526.5\" y=\"-132.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">X_train</text>\n",
       "<text text-anchor=\"start\" x=\"515.5\" y=\"-104.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- X_train&#45;&gt;trained_model -->\n",
       "<g id=\"edge15\" class=\"edge\">\n",
       "<title>X_train&#45;&gt;trained_model</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M603.58,-101.69C609.81,-99.08 616.3,-96.37 622.83,-93.64\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"624.53,-96.72 632.41,-89.63 621.83,-90.26 624.53,-96.72\"/>\n",
       "</g>\n",
       "<!-- train_predictions -->\n",
       "<g id=\"node13\" class=\"node\">\n",
       "<title>train_predictions</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M938.5,-94C938.5,-94 807.5,-94 807.5,-94 801.5,-94 795.5,-88 795.5,-82 795.5,-82 795.5,-42 795.5,-42 795.5,-36 801.5,-30 807.5,-30 807.5,-30 938.5,-30 938.5,-30 944.5,-30 950.5,-36 950.5,-42 950.5,-42 950.5,-82 950.5,-82 950.5,-88 944.5,-94 938.5,-94\"/>\n",
       "<text text-anchor=\"start\" x=\"806.5\" y=\"-72.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">train_predictions</text>\n",
       "<text text-anchor=\"start\" x=\"851.5\" y=\"-44.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- X_train&#45;&gt;train_predictions -->\n",
       "<g id=\"edge20\" class=\"edge\">\n",
       "<title>X_train&#45;&gt;train_predictions</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M603.92,-121.48C647.18,-120.11 711.76,-115.82 766.5,-103 773.41,-101.38 780.46,-99.41 787.49,-97.2\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"788.62,-100.51 797.03,-94.07 786.44,-93.86 788.62,-100.51\"/>\n",
       "</g>\n",
       "<!-- y -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>y</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M249,-154C249,-154 208,-154 208,-154 202,-154 196,-148 196,-142 196,-142 196,-102 196,-102 196,-96 202,-90 208,-90 208,-90 249,-90 249,-90 255,-90 261,-96 261,-102 261,-102 261,-142 261,-142 261,-148 255,-154 249,-154\"/>\n",
       "<text text-anchor=\"start\" x=\"223.5\" y=\"-132.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">y</text>\n",
       "<text text-anchor=\"start\" x=\"207\" y=\"-104.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- split_dataset -->\n",
       "<g id=\"node9\" class=\"node\">\n",
       "<title>split_dataset</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M463.5,-154C463.5,-154 363.5,-154 363.5,-154 357.5,-154 351.5,-148 351.5,-142 351.5,-142 351.5,-102 351.5,-102 351.5,-96 357.5,-90 363.5,-90 363.5,-90 463.5,-90 463.5,-90 469.5,-90 475.5,-96 475.5,-102 475.5,-102 475.5,-142 475.5,-142 475.5,-148 469.5,-154 463.5,-154\"/>\n",
       "<text text-anchor=\"start\" x=\"362.5\" y=\"-132.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">split_dataset</text>\n",
       "<text text-anchor=\"start\" x=\"400.5\" y=\"-104.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- y&#45;&gt;split_dataset -->\n",
       "<g id=\"edge13\" class=\"edge\">\n",
       "<title>y&#45;&gt;split_dataset</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M261.38,-122C283.45,-122 313.72,-122 341.37,-122\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"341.42,-125.5 351.42,-122 341.42,-118.5 341.42,-125.5\"/>\n",
       "</g>\n",
       "<!-- X -->\n",
       "<g id=\"node6\" class=\"node\">\n",
       "<title>X</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M266,-236C266,-236 191,-236 191,-236 185,-236 179,-230 179,-224 179,-224 179,-184 179,-184 179,-178 185,-172 191,-172 191,-172 266,-172 266,-172 272,-172 278,-178 278,-184 278,-184 278,-224 278,-224 278,-230 272,-236 266,-236\"/>\n",
       "<text text-anchor=\"start\" x=\"223\" y=\"-214.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">X</text>\n",
       "<text text-anchor=\"start\" x=\"190\" y=\"-186.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- X&#45;&gt;split_dataset -->\n",
       "<g id=\"edge12\" class=\"edge\">\n",
       "<title>X&#45;&gt;split_dataset</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M278.3,-182.62C292.56,-176.34 308.17,-169.42 322.5,-163 328.71,-160.22 335.16,-157.31 341.61,-154.39\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"343.4,-157.43 351.06,-150.11 340.51,-151.05 343.4,-157.43\"/>\n",
       "</g>\n",
       "<!-- X_test -->\n",
       "<g id=\"node7\" class=\"node\">\n",
       "<title>X_test</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M737,-296C737,-296 662,-296 662,-296 656,-296 650,-290 650,-284 650,-284 650,-244 650,-244 650,-238 656,-232 662,-232 662,-232 737,-232 737,-232 743,-232 749,-238 749,-244 749,-244 749,-284 749,-284 749,-290 743,-296 737,-296\"/>\n",
       "<text text-anchor=\"start\" x=\"675\" y=\"-274.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">X_test</text>\n",
       "<text text-anchor=\"start\" x=\"661\" y=\"-246.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">DataFrame</text>\n",
       "</g>\n",
       "<!-- X_test&#45;&gt;test_scatter_plot -->\n",
       "<g id=\"edge9\" class=\"edge\">\n",
       "<title>X_test&#45;&gt;test_scatter_plot</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M749.34,-269.95C800.05,-274.88 881.48,-279.29 950.5,-267 958.82,-265.52 967.33,-263.46 975.76,-261.05\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"977.05,-264.32 985.6,-258.07 975.02,-257.62 977.05,-264.32\"/>\n",
       "</g>\n",
       "<!-- test_predictions -->\n",
       "<g id=\"node12\" class=\"node\">\n",
       "<title>test_predictions</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M935.5,-176C935.5,-176 810.5,-176 810.5,-176 804.5,-176 798.5,-170 798.5,-164 798.5,-164 798.5,-124 798.5,-124 798.5,-118 804.5,-112 810.5,-112 810.5,-112 935.5,-112 935.5,-112 941.5,-112 947.5,-118 947.5,-124 947.5,-124 947.5,-164 947.5,-164 947.5,-170 941.5,-176 935.5,-176\"/>\n",
       "<text text-anchor=\"start\" x=\"809.5\" y=\"-154.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">test_predictions</text>\n",
       "<text text-anchor=\"start\" x=\"851.5\" y=\"-126.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">Series</text>\n",
       "</g>\n",
       "<!-- X_test&#45;&gt;test_predictions -->\n",
       "<g id=\"edge18\" class=\"edge\">\n",
       "<title>X_test&#45;&gt;test_predictions</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M735.7,-231.82C753.05,-216.73 774.69,-199.02 795.5,-185 797.19,-183.86 798.91,-182.73 800.66,-181.61\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"802.92,-184.32 809.58,-176.08 799.24,-178.37 802.92,-184.32\"/>\n",
       "</g>\n",
       "<!-- split_dataset&#45;&gt;y_train -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>split_dataset&#45;&gt;y_train</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M468.84,-89.83C481.53,-82.32 494.91,-74.39 507.18,-67.13\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"509.07,-70.07 515.9,-61.97 505.51,-64.05 509.07,-70.07\"/>\n",
       "</g>\n",
       "<!-- split_dataset&#45;&gt;y_test -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>split_dataset&#45;&gt;y_test</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M459.63,-154.01C485.79,-171.18 519.99,-191.36 553,-204\"/>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M555,-204C579.77,-213.49 746.31,-221.14 827.57,-224.36\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"827.85,-227.87 837.98,-224.76 828.12,-220.88 827.85,-227.87\"/>\n",
       "</g>\n",
       "<!-- split_dataset&#45;&gt;X_train -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>split_dataset&#45;&gt;X_train</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M475.65,-122C481.78,-122 487.99,-122 494.1,-122\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"494.23,-125.5 504.23,-122 494.23,-118.5 494.23,-125.5\"/>\n",
       "</g>\n",
       "<!-- split_dataset&#45;&gt;X_test -->\n",
       "<g id=\"edge8\" class=\"edge\">\n",
       "<title>split_dataset&#45;&gt;X_test</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M555,-204C583.41,-214.88 614.61,-227.81 640.64,-238.87\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"639.41,-242.15 649.98,-242.85 642.15,-235.71 639.41,-242.15\"/>\n",
       "</g>\n",
       "<!-- trained_model&#45;&gt;test_predictions -->\n",
       "<g id=\"edge17\" class=\"edge\">\n",
       "<title>trained_model&#45;&gt;test_predictions</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M766.81,-93.7C776.31,-98.25 786.14,-102.95 795.81,-107.57\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"794.49,-110.82 805.02,-111.98 797.51,-104.5 794.49,-110.82\"/>\n",
       "</g>\n",
       "<!-- trained_model&#45;&gt;train_predictions -->\n",
       "<g id=\"edge19\" class=\"edge\">\n",
       "<title>trained_model&#45;&gt;train_predictions</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M766.81,-62C772.78,-62 778.9,-62 785.01,-62\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"785.3,-65.5 795.3,-62 785.3,-58.5 785.3,-65.5\"/>\n",
       "</g>\n",
       "<!-- load_data -->\n",
       "<g id=\"node11\" class=\"node\">\n",
       "<title>load_data</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M93.5,-195C93.5,-195 18.5,-195 18.5,-195 12.5,-195 6.5,-189 6.5,-183 6.5,-183 6.5,-143 6.5,-143 6.5,-137 12.5,-131 18.5,-131 18.5,-131 93.5,-131 93.5,-131 99.5,-131 105.5,-137 105.5,-143 105.5,-143 105.5,-183 105.5,-183 105.5,-189 99.5,-195 93.5,-195\"/>\n",
       "<text text-anchor=\"start\" x=\"17.5\" y=\"-173.8\" font-family=\"Helvetica,sans-Serif\" font-weight=\"bold\" font-size=\"14.00\">load_data</text>\n",
       "<text text-anchor=\"start\" x=\"43\" y=\"-145.8\" font-family=\"Helvetica,sans-Serif\" font-style=\"italic\" font-size=\"14.00\">dict</text>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;y -->\n",
       "<g id=\"edge6\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;y</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M105.86,-151.25C131.17,-145.16 161.76,-137.81 185.93,-132\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"186.95,-135.35 195.86,-129.61 185.32,-128.54 186.95,-135.35\"/>\n",
       "</g>\n",
       "<!-- load_data&#45;&gt;X -->\n",
       "<g id=\"edge7\" class=\"edge\">\n",
       "<title>load_data&#45;&gt;X</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M105.86,-174.75C125.56,-179.49 148.46,-184.99 169.04,-189.94\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"168.39,-193.39 178.93,-192.32 170.02,-186.58 168.39,-193.39\"/>\n",
       "</g>\n",
       "<!-- test_predictions&#45;&gt;test_performance -->\n",
       "<g id=\"edge5\" class=\"edge\">\n",
       "<title>test_predictions&#45;&gt;test_performance</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M947.76,-144C955.72,-144 963.88,-144 972,-144\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"972.24,-147.5 982.24,-144 972.24,-140.5 972.24,-147.5\"/>\n",
       "</g>\n",
       "<!-- test_predictions&#45;&gt;test_scatter_plot -->\n",
       "<g id=\"edge11\" class=\"edge\">\n",
       "<title>test_predictions&#45;&gt;test_scatter_plot</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M947.49,-176.06C957.93,-180.61 968.72,-185.31 979.31,-189.93\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"977.99,-193.18 988.56,-193.97 980.79,-186.76 977.99,-193.18\"/>\n",
       "</g>\n",
       "<!-- train_predictions&#45;&gt;train_performance -->\n",
       "<g id=\"edge22\" class=\"edge\">\n",
       "<title>train_predictions&#45;&gt;train_performance</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M950.74,-49.75C956.79,-48.79 962.93,-47.81 969.08,-46.83\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"970.06,-50.21 979.39,-45.18 968.96,-43.3 970.06,-50.21\"/>\n",
       "</g>\n",
       "<!-- _split_dataset_inputs -->\n",
       "<g id=\"node15\" class=\"node\">\n",
       "<title>_split_dataset_inputs</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"322.5,-71.5 134.5,-71.5 134.5,-26.5 322.5,-26.5 322.5,-71.5\"/>\n",
       "<text text-anchor=\"start\" x=\"149.5\" y=\"-44.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">test_size_fraction</text>\n",
       "<text text-anchor=\"start\" x=\"276.5\" y=\"-44.8\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">float</text>\n",
       "</g>\n",
       "<!-- _split_dataset_inputs&#45;&gt;split_dataset -->\n",
       "<g id=\"edge14\" class=\"edge\">\n",
       "<title>_split_dataset_inputs&#45;&gt;split_dataset</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M297.59,-71.58C306.02,-74.63 314.49,-77.81 322.5,-81 328.82,-83.52 335.34,-86.24 341.85,-89.05\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"340.79,-92.4 351.35,-93.2 343.59,-85.99 340.79,-92.4\"/>\n",
       "</g>\n",
       "<!-- input -->\n",
       "<g id=\"node16\" class=\"node\">\n",
       "<title>input</title>\n",
       "<polygon fill=\"#ffffff\" stroke=\"black\" stroke-dasharray=\"5,2\" points=\"85.5,-305.5 26.5,-305.5 26.5,-268.5 85.5,-268.5 85.5,-305.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"56\" y=\"-283.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">input</text>\n",
       "</g>\n",
       "<!-- function -->\n",
       "<g id=\"node17\" class=\"node\">\n",
       "<title>function</title>\n",
       "<path fill=\"#b4d8e4\" stroke=\"black\" d=\"M84,-250.5C84,-250.5 28,-250.5 28,-250.5 22,-250.5 16,-244.5 16,-238.5 16,-238.5 16,-225.5 16,-225.5 16,-219.5 22,-213.5 28,-213.5 28,-213.5 84,-213.5 84,-213.5 90,-213.5 96,-219.5 96,-225.5 96,-225.5 96,-238.5 96,-238.5 96,-244.5 90,-250.5 84,-250.5\"/>\n",
       "<text text-anchor=\"middle\" x=\"56\" y=\"-228.3\" font-family=\"Helvetica,sans-Serif\" font-size=\"14.00\">function</text>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<graphviz.graphs.Digraph at 0x7f89d912e010>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%%cell_to_module model_training_2 --display\n",
    "import pandas as pd\n",
    "import matplotlib.figure\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.base import BaseEstimator\n",
    "from sklearn.datasets import fetch_openml\n",
    "from sklearn.linear_model import LogisticRegression, LinearRegression\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.metrics import balanced_accuracy_score\n",
    "from hamilton.function_modifiers import extract_fields\n",
    "\n",
    "\n",
    "@extract_fields(dict(X=pd.DataFrame, y=pd.Series))\n",
    "def load_data() -> dict:\n",
    "    \"\"\"Load the titanic dataset and split it in X and y. \n",
    "    Only keep the columns `fare` and `age` and fill null values.\n",
    "    \"\"\"\n",
    "    X, y = fetch_openml(\"titanic\", version=1, as_frame=True, return_X_y=True)\n",
    "    X = X[[\"fare\", \"age\"]].fillna(0)\n",
    "    return dict(X=X, y=y)\n",
    "\n",
    "\n",
    "@extract_fields(dict(\n",
    "    X_train=pd.DataFrame, y_train=pd.Series,\n",
    "    X_test=pd.DataFrame, y_test=pd.Series,\n",
    "))\n",
    "def split_dataset(\n",
    "    X: pd.DataFrame,\n",
    "    y: pd.Series,\n",
    "    test_size_fraction: float = 0.3\n",
    ") -> dict:\n",
    "    \"\"\"Partition the dataset into training and testing sets.\"\"\"\n",
    "    X_train, X_test, y_train, y_test = train_test_split(\n",
    "        X, y, test_size=test_size_fraction,\n",
    "    )\n",
    "    return dict(X_train=X_train, y_train=y_train, X_test=X_test, y_test=y_test)\n",
    "\n",
    "def trained_model(X_train: pd.DataFrame, y_train: pd.Series) -> BaseEstimator:\n",
    "    \"\"\"Binary classifier fitted on the training data\"\"\"\n",
    "    model = LogisticRegression()\n",
    "    model.fit(X_train, y_train)\n",
    "    return model\n",
    "\n",
    "def train_predictions(trained_model: BaseEstimator, X_train: pd.DataFrame) -> pd.Series:\n",
    "    return trained_model.predict(X_train)\n",
    "\n",
    "def train_performance(y_train: pd.Series, train_predictions: pd.Series) -> float:\n",
    "    \"\"\"Balanced accuracy on the training set\"\"\"\n",
    "    return balanced_accuracy_score(y_train, train_predictions)\n",
    "\n",
    "def test_predictions(trained_model: BaseEstimator, X_test: pd.DataFrame) -> pd.Series:\n",
    "    return trained_model.predict(X_test)\n",
    "\n",
    "def test_performance(y_test: pd.Series, test_predictions: pd.Series) -> float:\n",
    "    \"\"\"Balanced accuracy on the training set\"\"\"\n",
    "    return balanced_accuracy_score(y_test, test_predictions)\n",
    "\n",
    "def test_scatter_plot(\n",
    "    X_test: pd.DataFrame,\n",
    "    y_test: pd.Series,\n",
    "    test_predictions: pd.Series,\n",
    ") -> matplotlib.figure.Figure:\n",
    "    \"\"\"Scatter plot of fare and age with colors for correct/incorrect predictions\"\"\"\n",
    "    correctly_predicted = y_test == test_predictions\n",
    "    feature_1 = X_test.iloc[:, 0]\n",
    "    feature_2 = X_test.iloc[:, 1]\n",
    "\n",
    "    fig = plt.figure()\n",
    "    plt.scatter(feature_1, feature_2, c=correctly_predicted)\n",
    "    return fig"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.2 Assemble\n",
    "This code is just like Section #1.2, but we add a `MLFlowTracker()` to the `Driver` by passing it to `.with_materializers()`. This objects accepts many arguments to set the right tracking and registry server, specify the experiment names, and set other metadata. Generally, the defaults are sufficient if you're developing locally. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "from hamilton import driver\n",
    "from hamilton.io.materialization import to\n",
    "from hamilton.plugins.h_mlflow import MLFlowTracker\n",
    "\n",
    "dr = (\n",
    "    driver.Builder()\n",
    "    .with_modules(model_training_2)\n",
    "    .with_adapters(MLFlowTracker())\n",
    "    .with_materializers(\n",
    "        to.mlflow(\n",
    "            id=\"trained_model__mlflow\",\n",
    "            dependencies=[\"trained_model\"],\n",
    "            register_as=\"my_new_model\",\n",
    "        ),\n",
    "    )\n",
    "    .build()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[0;31mInit signature:\u001b[0m\n",
      "\u001b[0mMLFlowTracker\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mtracking_uri\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mregistry_uri\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0martifact_location\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mexperiment_name\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'Hamilton'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mexperiment_tags\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdict\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mexperiment_description\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mrun_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mrun_name\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mrun_tags\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdict\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mrun_description\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m    \u001b[0mlog_system_metrics\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n",
      "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mDocstring:\u001b[0m      Driver adapter logging Hamilton execution results to an MLFlow server.\n",
      "\u001b[0;31mInit docstring:\u001b[0m\n",
      "Configure the MLFlow client and experiment for the lifetime of the tracker\n",
      "\n",
      ":param tracking_uri: Destination of the logged artifacts and metadata. It can be a filesystem, database, or server. [reference](https://mlflow.org/docs/latest/getting-started/tracking-server-overview/index.html)\n",
      ":param registry_uri: Destination of the registered models. By default it's the same as the tracking destination, but they can be different. [reference](https://mlflow.org/docs/latest/getting-started/registering-first-model/index.html)\n",
      ":param artifact_location: Root path on tracking server where experiment is stored\n",
      ":param experiment_name: MLFlow experiment name used to group runs.\n",
      ":param experiment_tags: Tags to query experiments programmatically (not displayed).\n",
      ":param experiment_description: Description of the experiment displayed\n",
      ":param run_id: Run id to log to an existing run (every execution logs to the same run)\n",
      ":param run_name: Run name displayed and used to query runs. You can have multiple runs with the same name but different run ids.\n",
      ":param run_tags: Tags to query runs and appears as columns in the UI for filtering and grouping. It automatically includes serializable inputs and Driver config.\n",
      ":param run_description: Description of the run displayed\n",
      ":param log_system_metrics: Log system metrics to display (requires additonal dependencies)\n",
      "\u001b[0;31mFile:\u001b[0m           ~/projects/dagworks/hamilton/hamilton/plugins/h_mlflow.py\n",
      "\u001b[0;31mType:\u001b[0m           ABCMeta\n",
      "\u001b[0;31mSubclasses:\u001b[0m     "
     ]
    }
   ],
   "source": [
    "MLFlowTracker?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.3 Execute\n",
    "Like before, we request nodes for execution. But this time, all requested nodes will be logged in MLFlow, not just model savers! Inputs and other metadata will also be automatically available (see next section for details)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Registered model 'my_new_model' already exists. Creating a new version of this model...\n",
      "Created version '6' of model 'my_new_model'.\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "results = dr.execute(\n",
    "    [\"trained_model__mlflow\", \"train_performance\", \"test_performance\", \"test_scatter_plot\"],\n",
    "    inputs=dict(test_size_fraction=0.3)\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Feature list\n",
    "A list of included features and cool things possible with the MLFlow plugin (in no particular order)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Automatically tracks `.execute(inputs=...)` and `Builder().with_config()` as MLFlow params. This creates columns that you can use to filter runs in the UI."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. The run tag `code_version` is automatically added by the `MLFlowTracker`. This allows you to know exactly what code was executed and group runs that use the same code, but vary in terms of inputs. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. Store the entire `HamiltonGraph` as an artifact `hamilton_graph.json`. This contains the source code of the executed dataflow."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4. Automatically log `plotly` and `matplotlib` figures as `.png` artifacts. For more control, you can use the `to.plotly()` and `to.plt()` savers. Notably, this allows you to save interactive plotly visualizations as HTML. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5. Use the `MLFlowTracker` to specify experiment metadata and run metadata that will help you browse the MLFlow UI and programatic search. `experiment_description` and `run_description` accept markdown strings.\n",
    "\n",
    "    ```python\n",
    "    MLFlowTracker(\n",
    "        experiment_name=...,\n",
    "        experiment_description=...,\n",
    "        run_name=...,\n",
    "        run_tags=...,\n",
    "        run_description=...,\n",
    "    )\n",
    "    ```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "6. The tags specified using the Hamilton decorator `@tag` on the model-producing function are stored in the MLFlow model registry\n",
    "    ```python\n",
    "    import pandas as pd\n",
    "    from hamilton.function_modifiers import tag\n",
    "    from sklearn.linear_model import LogisticRegression\n",
    "\n",
    "    @tag(team=\"forecast\", feature_set=\"v3\")\n",
    "    def trained_model(X_train: pd.DataFrame, y_train: pd.Series) -> LogisticRegression:\n",
    "        \"\"\"Fit a binary classifier on the training data\"\"\"\n",
    "        model = LogisticRegression()\n",
    "        model.fit(X_train, y_train)\n",
    "        return model\n",
    "\n",
    "    # ...\n",
    "\n",
    "    to.mlflow(\n",
    "        id=\"trained_model__mlflow\",\n",
    "        dependencies=[\"trained_model\"],\n",
    "        register_as=\"new_algo\",\n",
    "    ),\n",
    "    ```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7. Use the `MLFlowTracker` with the `HamiltonTracker`. You can link the two by matching:\n",
    "\n",
    "    - `experiment_name` == `project_id`; You can manually create an `experiment_id`, but you can set its name.\n",
    "    - `run_name` == `dag_name`; You can have multiple MLFlow runs with the same name\n",
    "\n",
    "    ```python\n",
    "    from hamilton import driver\n",
    "    from hamilton.io.materialization import to\n",
    "    from hamilton.plugins.h_mlflow import MLFlowTracker\n",
    "    from hamilton_sdk.adapters import HamiltonTracker\n",
    "\n",
    "    project_id = 3\n",
    "    dag_name = \"titanic_classifier_training\"\n",
    "\n",
    "    dr = (\n",
    "        driver.Builder()\n",
    "        .with_modules(model_training_2)\n",
    "        .with_adapters(\n",
    "            MLFlowTracker(\n",
    "                experiment_name=f\"hamilton-project-{project_id}\",\n",
    "                run_name=dag_name,\n",
    "            ),\n",
    "            HamiltonTracker(\n",
    "                username=\"my_username\",\n",
    "                project_id=project_id,\n",
    "                dag_name=dag_name,\n",
    "            )\n",
    "        )\n",
    "        .with_materializers(\n",
    "            to.mlflow(\n",
    "                id=\"trained_model__mlflow\",\n",
    "                dependencies=[\"trained_model\"],\n",
    "                register_as=\"my_new_model\",\n",
    "            ),\n",
    "        )\n",
    "        .build()\n",
    "    )\n",
    "    ```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "8. Log model performance with nested runs (e.g., cross-validation, hyperparameter tuning). This will require adding `mlflow` code in your dataflow definition though.\n",
    "    ```python\n",
    "    import mlflow\n",
    "    from sklearn.model_selection import KFold\n",
    "\n",
    "    def model_cross_validation(X: pd.DataFrame, y: pd.Series):\n",
    "        kfold = KFold(n_splits=3)\n",
    "\n",
    "        for train, test in kf.split(X):\n",
    "            X_train, X_test, y_train, y_test = X[train], X[test], y[train], y[test]\n",
    "\n",
    "            model = LogisticRegression()\n",
    "            model.fit(X_train, y_train)\n",
    "\n",
    "            test_pred = model.predict(X_test)\n",
    "            score = balanced_accuracy(y_test, test_pred)\n",
    "\n",
    "            with mlflow.start_run(nested=True):\n",
    "                mlflow.log_metric(\"balanced_accuracy\", score)\n",
    "                # ... could log plots, hyperparams, etc.\n",
    "    ```\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
